package org.mysterylab.utopiaframework.core.orm.hibernate3;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.persistence.NonUniqueResultException;

import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.hibernate.metadata.ClassMetadata;
import org.mysterylab.utopiaframework.util.reflect.ReflectionUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;

/**
 * 封装了 Hibernate3 原生 API 的 DAO 泛型基类. 通过 Hibernate 来操纵对象, 主要是 {@link Session}
 * 的一些方法的二次封装和 HQL 与 QBC 的一些简单检索.
 * 
 * @author zhouych
 * @param <T>
 *            DAO 操作的对象类型
 * @param <PK>
 *            主键类型
 */
public class BaseHibernateDao<T, PK extends Serializable> {

	protected Logger logger = LoggerFactory.getLogger(getClass());
	
	protected SessionFactory sessionFactory;
	
	protected Class<T> entityClass;
	
	/**
	 * 通过子类的泛型定义获得对象的类型. 比如:
	 * <pre>
	 * {@code
	 *  public class UserDao extends BaseHibernateDao<User, Long>
	 * }
	 * </pre>
	 */
	public BaseHibernateDao() {
		this.entityClass = ReflectionUtil.getSuperClassGenericType(getClass());
	}
	
	/**
	 * 跳过 DAO 层, 直接在 Service 层使用 BaseHibernateDao 的构造函数,
	 * 在构造函数中定义对象类型.
	 * <pre>
	 * {@code
	 * 	BaseHibernateDao<User, Long> userDao = new BaseHibernateDao<User, Long>(sessionFactory, User.class);
	 * }
	 * </pre>
	 * @param sessionFactory
	 * @param entityClass
	 */
	public BaseHibernateDao(final SessionFactory sessionFactory,
			final Class<T> entityClass) {
		this.sessionFactory = sessionFactory;
		this.entityClass = entityClass;
	}

	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}

	/**
	 * 当配置有 sessionFactory 的时候采用 Spring 的自动注入.
	 * 
	 * @param sessionFactory
	 */
	@Autowired
	public void setSessionFactory(final SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}
	
	/**
	 * 获取 Hibernate 的 Session. 注意在使用 {@link SessionFactory#getCurrentSession()}
	 * 方法的时候, Hibernate 的操作默认必须包含在一个 Transaction 中. 如果这些操作 CRUD
	 * 操作不被包含在一个具体的事务中, 则会抛出如下异常:
	 * <pre>
	 * org.hibernate.HibernateException: No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here
	 * </pre>
	 * 这里顺便说下  {@link SessionFactory#openSession()} 方法与其的区别: opensSession()
	 * 得到一个新的 session, 而 getCurrentSession() 则是从当前线程中得到事务开始时创建的
	 * Transaction 的那个 session, 如果事务没有正确启动的话, 就没有一个 session 被绑定到当前线程
	 * 
	 * @return
	 */
	public Session getSession() {
		return sessionFactory.getCurrentSession();
	}
	
	/**
	 * 取得对象的主键名.
	 * 
	 * @return
	 */
	public String getIdName() {
		ClassMetadata meta = getSessionFactory().getClassMetadata(entityClass);
		return meta.getIdentifierPropertyName();
	}

	/**
	 * 按 id 获取对象. 采用 {@link Session#load(Class, Serializable)} 方法在数据库中不存在与
	 * OID 对应的记录的时候, 会抛出 {@link ObjectNotFoundException} 的异常. 采用 get()
	 * 还是采用 load() 方法的区别在于前者是立即加载, 而后者是延迟加载, 使用的场合有:
	 * <ul>
	 * 	<li>1) 如果加载一个对象的目的是为了访问它的各个属性, 用 get() 方法.</li>
	 * 	<li>2) 如果加载一个对象的目的是为了删除它, 或者是为了建立与别的对象的关联关系, 用 load() 方法.</li>
	 * </ul>
	 * 比如以下代码:
	 * <pre>
	 *  Transaction tx = session.beginTransaction();
	 *  // 立即检索策略
	 *  Order order = (Order) session.get(Order.class, new Long(1));
	 *  // 延迟检索策略
	 *  Customer customer = (Customer) session.load(Customer.class, new Long(1));
	 *  // 建立 Ordre 与 Customer 的多对一单向关联关系
	 *  order.setCustomer(customer);
	 *  tx.commit();
	 *  // Session 不需要知道 Customer 的各个属性的值, 而只要知道 Customer 对象的 OID 即可生成如下 sql 语句:
	 *  // update ORDER set CUSTOMER_ID=1, ORDER_NUMBER=... where ID=1;
	 * </pre>
	 * 
	 * @param id
	 *            主键
	 * @return
	 * @see #get(Serializable)
	 * @see #delete(Serializable)
	 */
	@SuppressWarnings("unchecked")
	public T load(final PK id) {
		Assert.notNull(id, "id can not be null");
		return (T) getSession().load(entityClass, id);
	}
	
	/**
	 * 按 id 获取对象. 采用 {@link Session#get(Class, Serializable)} 方法在数据库不存在与
	 * OID 对应的记录的时候, 会返回 null.
	 * 
	 * @param id
	 *            主键
	 * @return
	 * @see #load(Serializable)
	 */
	@SuppressWarnings("unchecked")
	public T get(final PK id) {
		Assert.notNull(id, "id can not be null");
		return (T) getSession().get(entityClass, id);
	}
	
	/**
	 * 按 id 列表获取对象列表.
	 * 
	 * @param ids
	 * @return
	 */
	public List<T> get(final Collection<PK> ids) {
		return find(Restrictions.in(getIdName(), ids));
	}
	
	/**
	 * 获取全部对象.
	 * 
	 * @return
	 */
	public List<T> getAll() {
		return find();
	}
	
	/**
	 * 获取全部对象, 支持按属性排序. 采用 QBC 的检索方式.
	 * 
	 * @param orderByProperty
	 * @param isAsc
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<T> getAll(String orderByProperty, boolean isAsc) {
		Criteria c = createCriteria();
		if (isAsc) {
			c.addOrder(Order.asc(orderByProperty));
		} else {
			c.addOrder(Order.desc(orderByProperty));
		}
		return c.list();
	}
	
	/**
	 * 按属性查找对象列表, 匹配方式为相等. 采用 QBC 的检索方式.
	 * 
	 * @param propertyName
	 * @param value
	 * @return
	 */
	public List<T> findBy(final String propertyName, final Object value) {
		Assert.hasText(propertyName, "propertyName can not be null");
		Criterion criterion = Restrictions.eq(propertyName, value);
		return find(criterion);
	}
	
	/**
	 * 采用 QBC 的检索方式查询对象列表.
	 * 
	 * @param criterions
	 *            数量可变的 Criterion
	 */
	@SuppressWarnings("unchecked")
	public List<T> find(final Criterion... criterions) {
		return createCriteria(criterions).list();
	}
	
	/**
	 * 采用 QBC 的检索方式查询唯一对象. 可能有以下几种情况:
	 * <ul>
	 * 	<li>1) 如果有多个值抛 {@link NonUniqueResultException} 异常, 需要用 setMaxResults(1) 方法来限制.</li>
	 * 	<li>2) 如果有值且只有一个, 返回一个 Object.</li>
	 * 	<li>3) 如果没值, 返回 null.</li>
	 * </ul>
	 * 
	 * @param criterions
	 *            数量可变的 Criterion
	 */
	@SuppressWarnings("unchecked")
	public T findUnique(final Criterion... criterions) {
		return (T) createCriteria(criterions).setMaxResults(1).uniqueResult();
	}
	
	/**
	 * 按属性查找唯一对象, 匹配方式为相等. 采用 QBC 的检索方式.
	 * 
	 * @param propertyName
	 * @param value
	 * @return
	 * @see #findUnique(Criterion...)
	 */
	@SuppressWarnings("unchecked")
	public T findUniqueBy(final String propertyName, final Object value) {
		Assert.hasText(propertyName, "propertyName can not be null");
		Criterion criterion = Restrictions.eq(propertyName, value);
		return (T) createCriteria(criterion).setMaxResults(1).uniqueResult();
	}
	
	/**
	 * 采用 HQL 的检索方式查询对象列表.
	 * 
	 * @param values
	 *            数量可变的参数, 按顺序绑定
	 */
	@SuppressWarnings("unchecked")
	public <X> List<X> find(final String hql, final Object... values) {
		return createQuery(hql, values).list();
	}
	
	/**
	 * 采用 HQL 的检索方式查询对象列表.
	 * 
	 * @param values
	 *            命名参数, 按名称绑定
	 */
	@SuppressWarnings("unchecked")
	public <X> List<X> find(final String hql, final Map<String, ?> values) {
		return createQuery(hql, values).list();
	}
	
	/**
	 * 采用 HQL 的检索方式查询唯一对象.
	 * 
	 * @param values
	 *            数量可变的参数, 按顺序绑定
	 * @see #findUnique(Criterion...)
	 */
	@SuppressWarnings("unchecked")
	public <X> X findUnique(final String hql, final Object... values) {
		return (X) createQuery(hql, values).setMaxResults(1).uniqueResult();
	}
	
	/**
	 * 采用 HQL 的检索方式查询唯一对象.
	 * 
	 * @param values
	 *            命名参数, 按名称绑定
	 * @see #findUnique(Criterion...)
	 */
	@SuppressWarnings("unchecked")
	public <X> X findUnique(final String hql, final Map<String, ?> values) {
		return (X) createQuery(hql, values).setMaxResults(1).uniqueResult();
	}

	/**
	 * 保存新增或修改的对象.
	 * 
	 * @param entity
	 *            对象
	 */
	public void save(final T entity) {
		Assert.notNull(entity, "entity can not be null");
		getSession().saveOrUpdate(entity);
		logger.debug("save entity: {}", entity);
	}
	
	/**
	 * 删除对象.
	 * 
	 * @param entity
	 *            对象必须是 session 中的对象或含 id 属性的 transient 对象
	 */
	public void delete(final T entity) {
		Assert.notNull(entity, "entity can not be null");
		getSession().delete(entity);
		logger.debug("delete entity: {}", entity);
	}

	/**
	 * 按 id 删除对象.
	 * 
	 * @param id
	 *            主键
	 * @see #load(Serializable)
	 */
	public void delete(final PK id) {
		Assert.notNull(id, "id can not be null");
		delete(load(id));
		logger.debug("delete entity {},id is {}", entityClass.getSimpleName(), id);
	}
	
	/**
	 * 采用 HQL 的检索方式进行批量修改/删除操作.
	 * 
	 * @param values
	 *            数量可变的参数,按顺序绑定
	 * @return 更新记录数.
	 */
	public int batchExecute(final String hql, final Object... values) {
		return createQuery(hql, values).executeUpdate();
	}

	/**
	 * 采用 HQL 的检索方式进行批量修改/删除操作.
	 * 
	 * @param values
	 *            命名参数,按名称绑定
	 * @return 更新记录数.
	 */
	public int batchExecute(final String hql, final Map<String, ?> values) {
		return createQuery(hql, values).executeUpdate();
	}
	
	/**
	 * 根据查询 HQL 与参数列表创建 Query 对象. 与 find() 函数可进行更加灵活的操作. 比如:
	 * <pre>
	 *  Query query = session.createQuery("from Order o where o.customer=":customer and "
	 *      + "o.orderNumber like :orderNumber");
	 *  query.setParameter(1, customer);
	 *  query.setParameter(2, orderNumber);
	 * </pre>
	 * 上面的程序默认是使用了 Hibernate 的自动根据参数值的 Java 类型来进行对应的映射类型,
	 * 这样可以减少在第三个参数中指定 Java 类型的麻烦. 但是对于日期的 java.util.Date 类型,
	 * 会对应多种 Hibernate 映射类型, 如 Hibernate.DATE 或 Hibernate.TIMESTAMP,
	 * 因此必须在 setParameter() 方法中显式地指定到底对应那种 Hibernate 映射类型. 比如:
	 * <pre>
	 *  Query query = session.createQuery("from Customer c where c.birthday=:birthday");
	 *  query.setParameter("birthday", birthday, Hibernate.DATE);
	 * </pre>
	 * 
	 * @param values
	 *            数量可变的参数, 按顺序绑定
	 */
	public Query createQuery(final String queryString, final Object... values) {
		Assert.hasText(queryString, "queryString can not be null");
		Query query = getSession().createQuery(queryString);
		if (values != null) {
			for (int i = 0; i < values.length; i++) {
				// 按顺序绑定参数
				query.setParameter(i, values[i]);
			}
		}
		return query;
	}
	
	/**
	 * 根据查询 HQL 与参数列表创建 Query 对象. 与 find() 函数可进行更加灵活的操作. 比如:
	 * <pre>
	 *  Query query = session.createQuery("from Customer as c where c.name=:name "
	 *      + "and c.age=:age";
	 *  Customer customer = new Customer();
	 *  customer.setName("Tom");
	 *  customer.setAge(21);
	 *  // 命名参数中的 "name" 和 "age" 分别对应 Customer 类的 name 属性和 age 属性, 否则会抛异常
	 *  query.setProperties(customer);
	 * </pre>
	 * 在本方法中采用的是传递一个 {@link Map} 作为参数, 同样会根据 Map 的名来进行自动绑定. 另外,
	 * setProperties() 方法调用 setParameter() 方法, setParameter() 方法再根据 Customer
	 * 对象的属性的 Java 类型来判断 Hibernate 映射类型. 如果命名参数为日期类型, 则不能通过
	 * setProperties() 方法来绑定. 另外, 参数绑定对 null 是安全的, 比如如下代码不会抛异常:
	 * <pre>
	 *  String name = null;
	 *  session.createQuery("from Customer c where c.name=:name").setString("name", name).list();
	 * </pre>
	 * 上面的查询语句对应的 SQL 语句是: 
	 * <pre>
	 *  select * from CUSTOMERS where NMAE=null;
	 * </pre>
	 * 这条查询语句的查询结果永远为空. 如果要查询名字为 null 的客户, 应该使用 "is null"
	 * 比较运算符, 比如:
	 * <pre>
	 *  // HQL 检索方式
	 *  session.createQuery("from Customer c where c.name is null");
	 *  // QBC 检索方式
	 *  session.createCriteria(Customer.class).add(Restrictions.isNull("name"));
	 * </pre>
	 * 
	 * @param values
	 *            命名参数, 按名称绑定
	 * @see #find(String, Object...)
	 */
	public Query createQuery(final String queryString, final Map<String, ?> values) {
		Assert.hasText(queryString, "queryString can not be null");
		Query query = getSession().createQuery(queryString);
		if (values != null) {
			// 把命名参数与对象的属性值进行绑定
			query.setProperties(values);
		}
		return query;
	}
	
	/**
	 * 根据 Criterion 条件创建 Criteria. 与 find() 函数可进行更加灵活的操作.
	 * 
	 * @param criterions
	 *            数量可变的 Criterion
	 */
	public Criteria createCriteria(final Criterion... criterions) {
		Criteria criteria = getSession().createCriteria(entityClass);
		for (Criterion c : criterions) {
			criteria.add(c);
		}
		return criteria;
	}
	
	/**
	 * 初始化对象. 使用 load() 方法得到的仅是对象 Proxy, 在传到 View 层前需要进行初始化.
	 * 如果传入 entity, 则只初始化 entity 的直接属性, 但不会初始化延迟加载的关联集合和属性.
	 * 如需初始化关联属性,需执行:
	 * <pre>
	 * Hibernate.initialize(user.getRoles())        // 初始化User的直接属性和关联集合
	 * Hibernate.initialize(user.getDescription())  // 初始化User的直接属性和延迟加载的Description属性
	 * </pre>
	 */
	public void initProxyObject(Object proxy) {
		Hibernate.initialize(proxy);
	}
	
	/**
	 * 为 Query 添加 distinct transformer. 预加载关联对象的 HQL 会引起主对象重复,
	 * 需要进行 distinct 处理.
	 * 
	 * @param query
	 * @return
	 */
	public Query distinct(Query query) {
		query.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
		return query;
	}
	
	/**
	 * 为 Criteria 添加 distinct transformer. 预加载关联对象的 HQL 会引起主对象重复,
	 * 需要进行 distinct 处理.
	 * 
	 * @param criteria
	 * @return
	 */
	public Criteria distinct(Criteria criteria) {
		criteria.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
		return criteria;
	}
	
	/**
	 * 判断对象的属性值在数据库内是否唯一.<br>
	 * 在修改对象的情景下, 如果属性新修改的值 (value) 等于属性原来的值 (orgValue) 则不作比较.
	 * 
	 * @param propertyName
	 * @param newValue
	 * @param oldValue
	 * @return
	 */
	public boolean isPropertyUnique(final String propertyName,
			final Object newValue, final Object oldValue) {
		if (newValue == null || newValue.equals(oldValue)) {
			return true;
		}
		Object object = findUniqueBy(propertyName, newValue);
		return (object == null);
	}
	
	/**
	 * 清除当前 Session.
	 */
	public void flush() {
		getSession().flush();
	}
}
